feat(spec)!: retire ListViewSchema.navigation.view, the detail-view binding nothing ever resolved - #18619
Conversation
WIP: tombstone + ADR-0087 registration. Generated artifacts follow. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Tombstone, ADR-0087 registration, regenerated artifacts and the pins. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a693aa4e1bac6118d3280c0d95fc8fa996c4593c && git checkout a693aa4e1bac6118d3280c0d95fc8fa996c4593c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 650c7938a962900518041f8aac7279e056166367 && git checkout -B drift-repro 21b7c12b4f779b86409db2a7bb9235fe80ef8a54 && git merge --no-ff 650c7938a962900518041f8aac7279e056166367
node scripts/docs-audit/affected-docs.mjs --json 21b7c12b4f779b86409db2a7bb9235fe80ef8a54
|
… the published README (objectui#9987) The README's record-navigation paragraph told an author to write the member that `@objectstack/spec` retired under ADR-0049 (objectstack-ai/objectstack#18619) — and then, in the same paragraph, explained the mechanism by which an undeclared key rejects the whole config. The first sentence caused what the second diagnosed. This file is listed in `files[]`, so the instruction shipped in the npm tarball. The instruction is replaced by the route the retirement actually leaves standing, carried in from `@object-ui/react`'s `useNavigationOverlay` docblock: assign a `record` page to the object and let `isDefault` pick the one that opens. Deleting the sentence alone was not enough — it answered a real authoring question, and a reader who loses the answer goes looking and finds the retired key elsewhere. The second sentence is kept byte-identical (modulo re-wrapping): it is the only place this README tells an author why a typo costs them the whole block. It just stops being a description of what the paragraph above it told them to do. `readme-navigation-example.test.ts` now measures the PROSE as well as the fence. The fence tests could never have caught this — they extract the ```json block, and the block never carried the key. The pinned `@objectstack/spec@17.4.0` still declares `view`, so nothing schema-derived can say it is retired in this tree; it is named by hand, exactly as `basePath` already is, and the absence carries its own control: the same detector is run over the sentence that used to carry the instruction and must find it there. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #16885
Clause-②: no
Retires
ListViewSchema.navigation.viewunder ADR-0049 enforce-or-remove, executing themaintainer ruling of 2026-09-13 (director decision batch #126 item 4, comment
5651023571,verbatim 「同意」, option B), with its item 5 superseded by comment
5657440531: thechangeset level is
minor, notmajor.What the key did
navigation.viewwas an unconstrained string whose describe promised "the form view to usefor details". No layer from spec to console ever resolved a view by that name. Its one read in
the shipped console passed the value into the second argument of
onNavigate— the slotthat otherwise carries the navigation-MODE token — so an authored name did not select a view,
it substituted for the mode. A consumer in the same bundle reads that argument against a
closed two-value vocabulary (
edit/view), so any other authored value matched neitherbranch. Declared, consumed, and wrong.
Acceptance criteria — executable, with both controls
PROBE —
navigation: { view: 'summary_view' }is now refused, at the key the author wrote,with the prescription naming page assignment as the route:
LIT CONTROL — the live siblings still parse.
{ mode: 'page' }is accepted, and so are allfive survivors together (
mode,preventNavigation,openNewTab,size,width). A tombstonethat broke its siblings would satisfy the refusal assertion while being a larger bug, and
navigationis one closed shape, so that blast radius is the whole block. Pinned at all threedoors —
ListViewSchema,ObjectListViewSchema, and the flattenedPUT /api/v1/meta/viewoverlay.
DARK CONTROL — the reading used is the regenerated
packages/spec/authorable-surface/ui.jsonui/NavigationConfig:*row set. It returned: 6 rows before, 6 rows after; exactly one linechanged,
ui/NavigationConfig:view->ui/NavigationConfig:view [RETIRED]; the retired keynever appears plain, and no other
ui/NavigationConfig:*row disappeared."ui/NavigationConfig:openNewTab", "ui/NavigationConfig:preventNavigation", "ui/NavigationConfig:size", - "ui/NavigationConfig:view", + "ui/NavigationConfig:view [RETIRED]", "ui/NavigationConfig:width",Gates observed RED before GREEN
A gate never observed failing for this change is not known to be a gate for it.
check:generated(check:docsleg)✗ 1 of 15 artifact(s) stale: content/docs/references/**✓ All 15 generated artifacts are up to date.after--fixregenerated exactly that onecheck:generated(check:api-surfaceleg)✗ ... dist/**/*.d.ts describe DIFFERENT sources than the ones on disk(stale-dist trap, after the test file moved)✓after a real rebuildcheck-adr-0087-registrationdeclares a breaking change (BREAKING) but no adr-0087: disposition marker— proven by committing the changeset with the marker line dropped1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... registered list-view-navigation-view-retiredThe ablation mutated
packages/spec/src/ui/view.zod.tson disk (tombstone occurrences 1 -> 0,ablation marker 0 -> 1, blob hash moved), ran the pins, then restored under a
trap— restoredblob hash byte-identical to
HEADandgit diff HEADempty. Note the direction: only the threerefusal pins went red; the eight sibling-acceptance assertions stayed green, which is the correct
shape for reverting a tombstone.
Gate results
pnpm --filter @objectstack/spec check:generated— all 15 green, including the four named onthe card:
check:authorable-surface,check:api-surface,check:docs,check:liveness. Workingtree clean afterwards (no regeneration drift).
pnpm --filter @objectstack/spec typecheck— green.pnpm --filter @objectstack/spec test—484 files, 13830 tests, all passed.
pnpm lint(repo-wide,eslint . --no-inline-config) —green, so no narrowing was taken and none is declared.
All 14 source audits
check:generateddeliberately does not run — green.check:skill-examplesfirst reported PREREQUISITE NOT MET (packages/client-react/distunbuiltin a fresh worktree); after building that closure it reads
✅ 258 prose examples type-check across 3 surface(s).node scripts/check-changeset-no-major.mjs— green, nomajorbump.check-widening-tellsdid not recur.node scripts/pm/check-widening-tells.mjs --declaration no --diff <this diff>exits 0:10 changed file(s) — 4 judged against a declared surface (no widening tell), 6 NOT MEASURED. The T1/T2 firing onretiredKey()lines recorded on #17955 and#17300 did not reproduce here, so there is nothing to report as a regression and the
declaration is unchanged, as ruled.
The ADR-0087 disposition is SEMANTIC, deliberately
The ruling asked for a semantic migration entry, and the shape earns it: a mechanical D2 strip
would delete the key without recording which list view lost it, and an author who wrote it
wanted a named detail layout — a want page assignment serves and a stripped key does not record.
So the entry is a D3
SemanticMigration,list-view-navigation-view-retired, and the tombstoneprescription therefore carries no
os migrate metasentence: that sentence is owed only wherea conversion covers the surface (
shared/retired-key.tsmodule docblock; the class pinretired-key-migrate-sentence.test.tsdeliberately judges nothing when the marker is absent).The precedent for registering a retired key with no D2 conversion is
data/AggregationNode:distinctin this same table.Registered as
ui/NavigationConfig:viewinRETIRED_KEYS_BY_MAJOR[18], which also starts itsaging clock. Note for the reviewer: no major-18 semantic entry reaches
spec-changes.jsonordocs/protocol-upgrade-guide.mdyet — measured, not assumed: four sampled major-18 ids(
change-management-family-retired,training-family-retired,scim-provider-object-retired,epoch-instant-keys-renamed) return 0 in both files, while major-17 ids return non-zero and theguide stops at "Protocol 17". This entry behaving the same way is the steady state for the open
window, not a gap.
Liveness ledger — nothing to update, and why
view/list/navigationis oneliverow with no classified children, becausecheck-livenessdrills one level. That is #17424's subject; it is cited here and not fixed, exactly as ruling
item 4 directs. Since the walk never reaches
navigation's children, no ledger row exists forviewto markdead, and none is owed —check:livenessis green with the tombstone in place,with no UNCLASSIFIED row.
packages/spec/liveness/**is untouched (held by #18582).The objectui contract twin is in the SIBLING repo — reported, not touched
Ruling item 3 asked where the contract twins live. They are in
objectui, not here. Thereading:
objectui/packages/types/src/__tests__/view-navigation-config-spec-parity.test.tsat the pinned.objectui-sha(53ded82bf7a494f54e344e19099dbf00854b8694) is the twin. It assertsEqual<ViewNavigationConfig, NavigationConfig>—ViewNavigationConfigis this spec type,re-exported by reference since objectui#4588.
const navigation: ViewNavigationConfig = { view: 'summary_view' };. Once objectui resolves aspec carrying this tombstone, those lines become
tscerrors under that package'stype-check.ViewNavigationConfigoccursonly in
packages/console/CHANGELOG.md(a release-owned file), and the trackedsdui.manifest.jsondeclaresnavigationas a flatobjectinput with no drill intoview,so
check:react-declaration-parityis structurally blind to it. Nothing here to update.The Console Pin Gate is not affected and does not run on this PR. Measured: that job is gated
on the
consolepaths filter (.objectui-shaplus four scripts), none of which this diff touches;and it runs
scripts/build-console.sh— a build, which does not type-check the sibling'stsconfig.test.json. objectui's src at the pinned SHA contains no value write ofnavigation.viewoutside that test file (only docblocks), and reads of.viewstill compileagainst
never | undefined. So this PR does not turnmainred.What is still owed, in the other repository: the twin gains the tombstone pin before objectui
picks up a spec carrying this retirement. Its dependency is
@objectstack/spec: ^17.0.0, so apublished 17.5.0 is in range.
File surface
Every path touched, including the pre-declared OPEN set.
packages/spec/src/ui/view.zod.tsretiredKey()tombstone + its prescription constantpackages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.tsRETIRED_KEYS_BY_MAJOR[18]registration (build-schemas gate (b))packages/spec/src/migrations/entries/semantic/18.list-view-navigation-view-retired.tspackages/spec/src/ui/view.test.tspackages/spec/src/migrations/registry.tsgen:migration-registry, never hand-edited.changeset/16885-retire-navigation-view.mdminor,**BREAKING**banner, FROM -> TO, ADR-0087 dispositionpackages/spec/authorable-surface/ui.jsonbuildcontent/docs/references/ui/view.mdxgen:docscontent/docs/references/api/protocol.mdxgen:docs(the inline navigation summary dropsview?: string)content/docs/references/data/object.mdxHeld by other in-flight work and not touched:
packages/spec/scripts/build-schemas.ts(#17969),packages/spec/liveness/**andpackages/spec/scripts/liveness/check-liveness.mts(#18582),packages/spec/src/api/protocol.zod.ts(#18597). Ruling item 2 was already discharged — PR #17796reads
closed, not merged — so the retirement is written fresh.Acceptance notes
Noted, not filed: nothing in the touched files.
One finding to file separately, unrelated to this diff and not fixed here:
pnpm check:cross-package-test-inputsreaches opposite verdicts on an identical source treedepending only on whether
packages/spec/dist/happens to be built — exit 1 with it present(115 entries), exit 0 with the same directory moved aside, same commit, restored afterwards.
The flagged radius is
packages/spec/dist/descended frompackages/cli/test/init-created-files-summary.e2e.test.ts, which this diff does not touch. Nopnpm buildstep precedespnpm check:cross-package-test-inputsinlint.yml'slintjob, soCI always runs it against an unbuilt tree and it passes vacuously — the declaration hole it exists
to catch is structurally invisible on every PR. That is the "a verifier that silently degrades is
worse than no verifier" shape from AGENTS.md Route & surface ownership rule 3.
维护者速读(草稿)
改了什么 ——
ListViewSchema.navigation.view退役:键仍在 shape 里,但变成 ADR-0049 墓碑,写它是
tsc报错、传值进来是带处方的 parse 拒收。处方点名替代路线:给对象发布record页面,用
isDefault决定打开哪一个。同批落地 ADR-0087 的 D3 语义迁移条目、changeset(minor+**BREAKING**)、以及三道门上的拒收 pin。为什么改 —— 这个键承诺"用哪个表单视图打开详情",而从 spec 到 console 没有任何一层按名字解析
视图。它唯一的读取把值塞进了
onNavigate的第二个参数 —— 那个槽位装的是导航模式令牌。所以作者写的视图名不是在选视图,而是在顶替模式;而同一个 bundle 里有消费者拿这个参数去对
edit/view两值词表,任何别的值两个分支都不匹配。仓库内零编写实例,唯一的外部作者已自行删除。维护者 2026-09-13 裁决 B:零拉力即移除。
风险与代价(含回滚) —— 风险低。行为面没有回归可言:这个键从来没选中过任何东西,决定详情怎么
呈现的是
mode和size,两者一字未动,并且有 pin 钉着五个存活键仍然接受。代价是发布面:这是breaking,升级时仍写着该键的元数据会在三道门(含
PUT /api/v1/meta/view)被拒。回滚就是revert 本 PR —— 墓碑、注册条目、changeset 与生成产物都在同一个提交序列里,没有跨仓副作用。
一个待办留在姊妹仓:objectui 的契约孪生 pin 把
{ view: 'summary_view' }写成合法值,需要在objectui 取到带墓碑的 spec 之前补上墓碑 pin。本 PR 不触发 Console Pin Gate,不会让
main变红。席位意见 ——
你要做的 ——
Generated by Claude Code